struct WrapperType[T] {
    public var innerValue: T;

    public static function f() {
        printf("this function ignores the type parameter");
    }

    public static function g(fmt: CString, v: T) {
        printf(fmt, v);
    }

    public function greet(fmt: CString) {
        printf("greeting: ");
        printf(fmt, this.innerValue);
    }

    public function greetWith(fmt: CString, other: T) {
        printf("greetings from ");
        printf(fmt, this.innerValue);
        printf(" and ");
        printf(fmt, other);
        printf("\n");
    }
}

union Either[A, B] {
    public var a: A;
    public var b: B;

    public function printLeft(fmt: CString) {
        printf(fmt, this.a);
    }

    public function printRight(fmt: CString) {
        printf(fmt, this.b);
    }
}

enum WrapperEnum[T] {
    OneValue(v: T);
    TwoValues(a: T, b: T);

    public function which(fmt: CString) {
        match this {
            OneValue(a) => {
                printf("one: ");
                printf(fmt, a);
                printf("\n");
            }
            TwoValues(a, b) => {
                printf("two: ");
                printf(fmt, a);
                printf(", ");
                printf(fmt, b);
                printf("\n");
            }
        }
    }
}

abstract WrapperAbstract[T]: T {
    public function hello(fmt: CString) {
        printf("Hello ");
        printf(fmt, this);
        printf("\n");
    }
}

function main() {
    // fully specified
    var a1: WrapperType[Bool] = struct WrapperType {
        innerValue: true,
    };
    var a2: WrapperType[Bool] = struct WrapperType {
        innerValue: false,
    };
    // incomplete parameters
    var b: WrapperType = struct WrapperType {
        innerValue: 1,
    };
    // var b2: WrapperType[_] = b;
    // full type inference
    var c = struct WrapperType {
        innerValue: 2,
    };
    var d = struct WrapperType {
        innerValue: "hello",
    };

    // generic struct field access
    printf("%i\n", a1.innerValue);
    printf("%i\n", a2.innerValue);
    printf("%i\n", b.innerValue);
    printf("%i\n", c.innerValue);
    printf("%s\n", d.innerValue);

    // generic methods
    a1.greet("%i\n");
    d.greet("%s\n");
    b.greetWith("%i", 5);

    // union with two inferred parameters
    var e: Either;
    e.a = 10;
    e.printLeft("%i\n");
    e.b = 20.5;
    e.printRight("%.2f\n");

    // union with partially specified parameters
    var f: Either[Int];
    f.a = 40;
    f.printLeft("%i\n");
    f.b = 77.77;
    f.printRight("%.2f\n");

    // TODO: static methods of generic types...?
    // WrapperType.f();
    // WrapperType.g("%s\n", "this function uses the type parameter");

    var g: WrapperEnum[Int] = OneValue(1);
    // generic pattern matching issue
    g.which("%i");
    var h: WrapperEnum[Float] = TwoValues(2.0, 3.0);
    h.which("%.1f");
    var i: WrapperEnum = OneValue(4);
    i.which("%i");
    var j: WrapperEnum = TwoValues(5, 6.0);
    j.which("%.1f");

    var k: WrapperAbstract[Int] = 1;
    var l: WrapperAbstract = "hello" as WrapperAbstract[CString];
    k.hello("%i");
    l.hello("%s");
}
